package kr.ac.kaist.savehearts.location;

import kr.ac.kaist.savehearts.SaveHeartsConstants;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;

/**
 * @brief UserLocationTracker records current location of the user periodically, 
 * 	not continuously to reduce the battery consumption. Update interval is specified in
 * 	Configure class, and which can be modified in GPSSettingsActivity.
 * 	UserLocationTracker tracks the user's location using GPS and network providers.
 * 	UserLocationTracker start tracking with startTracking(), stops tracking with stopTracking().
 * @author Seungwook Han (initialsw@kaist.ac.kr)
 */

public class UserLocationTracker {

	private static final String TAG = UserLocationTracker.class.getSimpleName();
	
	private static UserLocationTracker _instance;	
	public static UserLocationTracker getInstance(Context context) {
		if ( _instance == null ) _instance = new UserLocationTracker(context);
		return _instance;
	}
	
	/**
	 * Is tracker running or not?
	 */
	private boolean _bTrackerRunning = false;
	
	private LocationManager _locationManager = null;
	private LocationListener _locationListener = null;
	
	/**
	 * Last known location to get a fast fix
	 */
	private Location _lastKnownLocation = null;
	
	private Context _context;
	
	private int updateInterval = SaveHeartsConstants.DEFAULT_GPS_INTERVAL;
	public int getUpdateInterval() { return this.updateInterval; }
	public void setUpdateInterval(int interval) { this.updateInterval = interval; }
	
	private LocationTrackingListener mListener;
	public LocationTrackingListener getListener() { return this.mListener; }
	public void setListener(LocationTrackingListener listener) { this.mListener = listener; }
	
	private Location _lastLocation;
	private long _lastUpdateTime;
	
	public UserLocationTracker(Context context) {
		this._context = context;
		this._locationManager = (LocationManager)this._context.getSystemService(Context.LOCATION_SERVICE);
		
		// use shared preferences...
	}
	
	public Location getLastKnownLocation() { return this._lastKnownLocation; }
	public double getLatitude() { return this._lastLocation.getLatitude(); }
	public double getLongitude() { return this._lastLocation.getLongitude(); }
	public long getLastUpdateTime() { return this._lastUpdateTime; }
	
	/**
	 * @brief isTrackerRunning() returns the status of location tracker (or location listener)
	 * @return
	 * 		true: The location tracker is running
	 * 		false: The location tracker is not running
	 */
	public boolean isTrackerRunning() {		
		return this._bTrackerRunning;
	}
	
	/**
	 * @brief isTrackerRunning(boolean) updates the status of location tracker. 
	 * 		The status can be updated by UserLocationTracker itself.
	 * @param bStatus Current status of location tracker (true: running/false: not running)
	 */
	private void isTrackerRunning(boolean bStatus) {
		this._bTrackerRunning = bStatus;
	}
	
	/**
	 * @brief getLocationListener() returns location listener instance for tracking user location
	 * @return Instance of LocationListener class
	 */
	public LocationListener getLocationListener() {
		return this._locationListener;
	}
	
	/**
	 * @brief getLocationManager() returns location manager required for tracking user location
	 * @return Instance of LocationManager class
	 */
	public LocationManager getLocationManager() {
		return this._locationManager;
	}

	/**
	 * @brief startTracking() starts tracking user location by registering location listener to location manager.
	 * 		To reduce battery consumption, the location update occurs periodically (15 minutes in default).
	 * @return
	 * 		true: successfully started the tracker
	 * 		false: failed to start the tracker (Tracker is already running or necessary antecedents are not completed
	 */
	public boolean startTracking() {
		if ( getLocationManager() == null ) return false;
		
		// initialize location listener
		initLocationListener();
		
		try {		
			// Register the listener with the Location Manager to received location updates		
			getLocationManager().requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 
					this.getUpdateInterval(), 0, getLocationListener());		
			getLocationManager().requestLocationUpdates(LocationManager.GPS_PROVIDER, 
					this.getUpdateInterval(), 0, getLocationListener());
		} catch ( Exception e ) {
			e.printStackTrace();
			return false;
		}
		
		// the tracker is running
		isTrackerRunning(true);
		
		Log.d(TAG, "Tracking Started.");
		
		return true;
	}
	
	/**
	 * @brief stopTracking() stops tracking the user's location.
	 * @return 
	 * 		true: Successfully stopped the tracker
	 * 		false: failed to stop the tracker 
	 */
	public boolean stopTracking() {		
		if ( getLocationManager() == null ) return false; 
		if ( !isTrackerRunning() ) return false;
		
		try {
			// remove all listeners previously added
			getLocationManager().removeUpdates(getLocationListener());
		} catch ( Exception e ) {
			e.printStackTrace();
			return false;
		}
		
		isTrackerRunning(false);
		
		Log.d(TAG, "Tracking Stopped.");
		
		return true;
	}
	
	private final int TWO_MINUTES = 2 * 60 * 1000;
	
	/**
	 * @brief isBetterLocation() checks the updated location is best result considering accuracy of provider type.
	 * 		It determines whether one Location reading is better than the current Location fix
	 * 		For more information, refer to Android Developers @ http://developer.android.com/guide/topics/location/strategies.html
	 * @param location The new location that you want to evaluate
	 * @param currentBestLocation The current Location fix, to which you want to compare the new one
	 * @return
	 */
	protected boolean isBetterLocation(Location location, Location currentBestLocation) {
		if ( currentBestLocation == null ) {
			// A new location is always better than no location
			return true;
		}
		
		// Check whether the new location fix is newer or later
		long timeDelta = location.getTime() - currentBestLocation.getTime();
		boolean isSignificantNewer = timeDelta > TWO_MINUTES;
		boolean isSignificantOlder = timeDelta < -TWO_MINUTES;
		boolean isNewer = timeDelta > 0;
		
		// If it's been more than two minutes since the current location, use the new location 
		// because the user has likely moved
		if ( isSignificantNewer ) return true;
		// If the new location is more than two minutes older, it must be worse
		else if ( isSignificantOlder ) return false;
		
		// check whether the new location fix is more or less accurate
		int accuracyDelta = (int)(location.getAccuracy() - currentBestLocation.getAccuracy());
		boolean isLessAccurate = accuracyDelta > 0;
		boolean isMoreAccurate = accuracyDelta < 0;
		boolean isSignificantlyLessAccurate = accuracyDelta > 200;
		
		// Check if the old and new location are from the same provider
		boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());
		
		// Determine location quality using a combination of timeliness and accuracy
		if ( isMoreAccurate ) return true;
		else if ( isNewer && !isLessAccurate ) return true;
		else if ( isNewer && !isSignificantlyLessAccurate && isFromSameProvider ) return true;
		
		return false;
	}
	
	/**
	 * @brief isSameProvider() compares two location providers (name in String format)
	 * @param provider1 The name of first provider to compare with provider2
	 * @param provider2 The name of second provider to compare with provider1
	 * @return 
	 * 		true: Two providers are same
	 * 		false: Two providers are different
	 */
	private boolean isSameProvider(String provider1, String provider2) {
		if ( provider1 == null ) return provider2 == null;
		return provider1.equals(provider2);
	}
	
	/**
	 * @brief initLocationListener() initializes locationListener to start tracking user location.
	 * 		It failed to initialize locationListener when the tracker is running or locationListener already exists.
	 * @return
	 * 		true: successfully initialized locationListener
	 * 		false: failed to initialize locationListener because locationListener already exists
	 */
	private boolean initLocationListener() {
		if ( isTrackerRunning() ) return false;
		if ( getLocationListener() != null ) return false;
		
		Log.d(TAG, "initLocationListener()");
		
		this._locationListener = new LocationListener() {

			@Override
			public void onLocationChanged(Location location) {
				// if changed location is better than last (best) location, update the last location to current location
				if ( isBetterLocation(location, _lastLocation) ) 
					_lastLocation = location;
				_lastUpdateTime = System.currentTimeMillis();
				_lastKnownLocation = location;
				
				if ( mListener != null ) mListener.locationUpdated(location);
				
				Log.d(TAG, "onLocationChanged(" + location.getLongitude() + ", " + location.getLatitude() + ")");
			}

			@Override
			public void onProviderDisabled(String str) {
				Log.d(TAG, "onProviderDisabled()");
			}

			@Override
			public void onProviderEnabled(String str) {	
				Log.d(TAG, "onProviderEnabled()");
			}

			@Override
			public void onStatusChanged(String str, int val, Bundle bundle) {
				Log.d(TAG, "onStatusChanged()");
			}			
		};
		
		return true;
	}
	
	private boolean resetLocationListener() {
		this._locationListener = null;
		return true;
	}	
	
}
